<?php
namespace phiction;
use \phiction\string_utils as str;

// find all test sets
// for each test set:
//   tokenize
//   find all test cases ( *_ok or *_fail )
//   try { run test case } catch (Throwable) {}
class tester
{
    private $invert_negative;
    private $term;
    private $test_sets = [];
    private $total_case = 0;
    private $failed_cases = [];
    private $ok_outputs = [];

    // find all test sets
    function __construct(config $config, terminal $term, string $path=null, string $regex='/\/__test__\/.*\.php$/')
    {
        $this->invert_negative = $config->invert_negative;
        $this->term = $term;

        if (is_null($path)) $path = getcwd();
        $path_len = strlen($path);
        echo $this->wrap(2, $term->kv("scan ", $path)) . "\n";

        foreach (self::scan($path, $regex) as $name) {
            $short_name = substr($name, $path_len);
            $this->load($short_name, $name);
        }
    }

    function load($short_name, $path)
    {
        $term = $this->term;
        echo $this->wrap(2, $term->kv("load ", $short_name)) . "\n";

        // load and catch errors
        ob_start();
        set_error_handler(function ($errno, $msg) {
            throw new \Exception($msg, $errno);
        }, E_ALL);
        $failure = null;
        try {
            $test_set = require_once $path;
        }
        catch (\Throwable $e) {
            $failure = $e;
        }
        restore_error_handler();
        $output = ob_get_clean();

        if (is_null($failure)) {
            if (!is_array($test_set)) $test_set = [$test_set];
            $this->test_sets[$short_name] = $test_set;

            if ($output !== '') {
                $this->total_case++;
                $this->ok_outputs[$short_name] = $output;
            }
        } else {
            $this->total_case++;
            echo $this->wrap(2, $term->kv("FAILED LOADING ", $short_name, $term::H . $term::R)) . "\n";
            $this->failed_cases[$short_name] = [
                'exception' => $failure,
                'output' => $output,
            ];
        }
    }

    function run()
    {
        $this->test_all();
        $this->print_result();
    }


    function test_all()
    {
        foreach ($this->test_sets as $name => $set)
            $this->test_set($name, $set);
    }

    function test_set($set_name, $set)
    {
        foreach ($set as $name => $case)
            $this->test_case($set_name, $name, $case);
    }

    function test_case($set_name, $case_short_name, $case)
    {
        $term = $this->term;

        // prepare
        $case_name = "${set_name}[$case_short_name]";
        echo $this->wrap(8, $term->kv("      test ", $case_name));

        // testing
        ob_start();
        set_error_handler(function ($errno, $msg) {
            throw new \Exception($msg, $errno);
        }, E_ALL);
        $failure = null;
        try {
            $case();
        }
        catch (\Throwable $e) {
            $failure = $e;
        }
        restore_error_handler();
        $output = ob_get_clean();

        // negate result when necessary
        if (substr($case_short_name, 0, 1) === '!' && !$this->invert_negative) {
            $failure = is_null($failure) ? new \Exception("passed the test") : null;
        }

        // save result
        $this->total_case++;
        if (is_null($failure)) {
            echo $term->color($term::H . $term::G, "\r   OK") . "\n";
            if ($output !== '') $this->ok_outputs[$case_name] = $output;
        }
        else {
            echo $term->color($term::H . $term::R, "\r FAIL") . "\n";
            $this->failed_cases[$case_name] = [
                'exception' => $failure,
                'output' => $output,
            ];
        }
    }


    function print_result()
    {
        $this->print_ok_outputs();
        $this->print_fail_cases();
        $this->print_statistics();
        echo "\n";
    }

    function print_statistics()
    {
        $term = $this->term;

        $num_total = $this->total_case;
        $num_fail = count($this->failed_cases);
        $num_ok = $num_total - $num_fail;
        echo "\n";
        echo $term->color($term::RG, str_repeat('-', $term->width())) . "\n";
        echo $this->wrap(2, $term->color($term::H . $term::RG, "STATISTICS")) . "\n";
        echo $this->wrap(4, $term->kv("* total: ", $num_total)) . "\n";
        echo $this->wrap(4, $term->kv("* fail : ", $num_fail) . ($num_fail === 0 ? $term->color($term::H . $term::G, " GOOD") : "")) ."\n";
        echo $this->wrap(4, $term->kv("* ok   : ", $num_ok)) . "\n";
        foreach (array_keys($this->failed_cases) as $failed)
            echo $this->wrap(4, $term->kv("* FAIL ", $failed, $term::R)) . "\n";
    }

    function print_ok_outputs()
    {
        $term = $this->term;
        foreach ($this->ok_outputs as $case_name => $output) {
            echo "\n";
            echo $term->color($term::G, str_repeat('~', $term->width())) . "\n";
            echo $this->wrap(2, $term->kv("OK ", $case_name, $term::H . $term::G)) . "\n";
            echo $this->wrap(3, str::indent_by($output, $term->color($term::H, ":: "))) . "\n";
        }
    }

    function print_fail_cases()
    {
        $term = $this->term;
        foreach ($this->failed_cases as $case_name => $failure) {
            $ex = $failure['exception'];
            echo "\n";
            echo $term->color($term::R, str_repeat('@', $term->width())) . "\n";
            echo $this->wrap(2, $term->kv("FAIL ", $case_name, $term::H . $term::R) . " " . $term->color($term::H . $term::R, $ex->getMessage())) . "\n";
            echo $this->wrap(2, $term->kv("@ ", $ex->getLine(), $term::H . $term::R) . " " . $ex->getFile()) . "\n";
            echo $this->wrap(2, str::indent_by($ex->getTraceAsString(), $term->color($term::H . $term::R, "> "))) . "\n";

            $output = $failure['output'];
            if ($output !== '')
                echo str::indent_by($output, $term->color($term::H, "!! ")) . "\n";
        }
    }


    static function assert(bool $pred, string $message)
    {
        if ($pred) return;
        throw new \Exception($message);
    }

    static function must_equal($a, $b)
    {
        $a = var_export($a, true);
        $b = var_export($b, true);
        self::assert($a === $b, "$a === $b cannot hold true.");
    }


    private static function scan(string $path, string $regex): \Iterator
    {
        $dir = new \RecursiveDirectoryIterator($path);
        $it = new \RecursiveIteratorIterator($dir);
        $re = new \RegexIterator($it, $regex);
        return $re;
    }

    private function wrap(int $level, string $str): string
    {
        $term = $this->term;
        return str::wrap_lines(
            $str,
            $term->width(),
            str_repeat(' ', $level),
            $term->color($term::IRGB, " "));
    }
}

